ステート - Noise Protocol Framework (10)
Noiseプロトコルによるハンドシェイクでは、ハンドシェイクを行う両者はステートというものを生成・保持します。
ステートは3種類あります。
CipherState
内部に変数nとkをもつオブジェクトです。これらの変数は暗号、復号に使用されます。
kは暗号鍵、nはnonceと呼ばれるカウンター値です。kはDH鍵合意の結果から計算されます。nは暗号化/復号化が行われる度にインクリメントされていきます。
各パーティはハンドシェイクフェーズでは1つのCipherStateを持っていますが、ハンドシェイク確立後は2つのCipherStateを持ちます。
SymmetricState
CipherStateを内部に持ちます。加えてck変数とh変数を保持しています。
ckはchaining keyと呼ばれるもので、これ自体は暗号鍵ではありませんが、暗号鍵を導出する際に使用します。
ckはhkdf関数に渡すsaltとして使用します。hkdfの結果の先頭HASHLENバイトを次に使用するckとして設定します。
hはハンドシェイクで送受信するすべてのデータ(公開鍵など)をハッシュ化した値です。
これもハンドシェイク時にデータを送受信するごとに前回の値をベースにして更新していきます。
各パーティーが「対称暗号(=共通鍵暗号)」をできるようにするのがこのオブジェクトの役目です。
ハンドシェイクの間、各パーティーは1つのSymmetricStateを持ちますが、ハンドシェイク後はこのオブジェクトを破棄できます。
変数hについてはハンドシェイク確立後も使用するため、h自体は破棄してはいけません。
HandshakeState
SymmetricStateとDH変数(s、e、rs、re)、およびハンドシェークパターンを表す変数が含まれています。
DH変数は以下の通り
s: 自分の静的鍵のキーペア
e: 自分の一時鍵のキーペア
rs: 相手の静的公開鍵
re: 相手の一時公開鍵
ハンドシェイクフェーズの間、各パーティは1つのHandshakeStateを持ちますが、ハンドシェイク後はこのオブジェクトを破棄できます。
Noise Protocol Frameworkではそれぞれのステートオブジェクトが実装すべきプロパティ、メソッドを仕様として定めています。
CipherState
CipherStateは以下の2つのプロパティを持ちます。
k: 32バイトのバイナリです。空(empty)でもよいです。このkはデータを暗号化する際のキーとして利用します。
n: 8バイト(64ビット)のunsigned integerです。ノンスを意味し、暗号化(復号化)が行われるたびにインクリメントされていきます。
また、以下のメソッドを実装しています。
InitializeKey(key)
kの値を初期化するメソッドです。
HasKey()
kの値がすでに初期化されていればtrue、初期化されていなければfalseを返します。
SetNonce(nonce)
EncryptWithAd(ad, plaintext)
kが設定されていない場合は、plaintextをそのまま返します。それ以外の場合は暗号化関数であるENCRYPTを呼び出します。
暗号化に成功した場合はnの値をインクリメントします。
DecryptWithAd(ad, ciphertext)
kが設定されていない場合は、ciphertextをそのまま返します。それ以外の場合は復号化関数であるDECRYPTを呼び出します。
復号化に成功した場合はnの値をインクリメントします。
Rekey()
kの値を再生成します。
SymmetricState
SymmetricStateは以下の3つのプロパティを持ちます。
cipherState: CipherStateオブジェクトです
ck: HASHLENバイトのchaining keyです。
h: HASHLENバイトのハッシュ値です。
以下のメソッドを実装しています。
InitializeSymmetric(protocol_name)
内部の値ckとhを初期化します。protocol_nameがHASHLENより短い場合はprotocol_nameを0パディングしたもの、長い場合はprotocol_nameをハッシュしたものを設定します。
同じ値を設定するので、このメソッド実行直後は$ ck = h です。
MixKey(input_key_material)
鍵導出関数HKDFから新しい鍵を導出して、その鍵をCipherState#InitializeKey(key)を使って、CipherStateに設定します。
MixHash(data)
dataを使って、新しいh値を計算します。
MixKeyAndHash(input_key_material)
HKDFを使った鍵の導出とMixHashメソッドを使ったh値の計算を行います。
GetHandshakeHash():
内部に保持しているh値を返します。このメソッドはハンドシェイク完了後に呼び出すことを想定しています。
EncryptAndHash(plaintext)
CipherState#EncryptWithAd(h, plaintext)とMixHash(ciphertext)の呼び出しを行います。
DecryptAndHash(ciphertext)
CipherState#DecryptWithAd(h, ciphertext)とMixHash(ciphertext)の呼び出しを行います。
メソッドMixHashに渡すデータは復号された平文ではありません。暗号化されたデータをそのまま渡します。
Split()
トランスポートメッセージを暗号化/復号化するためのCipherStateオブジェクトを2つ生成します。
2つのうち1つが送信用で、もう一つが受信用です。一方向通信の場合などではこのうち1つだけを使用します。
HandshakeState
HandshakeStateは以下のプロパティを持ちます。
symmetricState: SymmetricStateオブジェクト
s: 自分の静的鍵のキーペア
e: 自分の一時鍵のキーペア
rs: 相手の静的公開鍵
re: 相手の一時公開鍵
initiator: イニシエーターの場合はtrue、レスポンダーの場合はfalseの値を返すboolean値
message_patterns: メッセージパターンの配列。
メッセージパターンとは、1回のハンドシェイクで行われる鍵の送受信とDH鍵合意をトークンと呼ばれる値("e", "s", "ee", "es", "se", "ss", "psk")の配列として表したものです。例えば、以下のようなハンドシェイクパターンの場合
NN:
<- e
-> e, ee
message_patternsの内容は以下のようなものになります。
code: message_patterns
message_patterns = [
]
ローカルの鍵であるsとeはキーペア(秘密鍵、公開鍵)の形式で保持しますが、相手の鍵はキーペアではなく公開鍵のみを保持します。
また、HandshakeStateには以下のメソッドが実装されています。
Initialize(handshake_pattern, initiator, prologue, s, e, rs, re)
HandshakeStateを与えられたパラメータをつかって初期化します。
handshake_patternに含まれるプレメッセージに対する処理(MixHashを呼び出す)もここで行います。
WriteMessage(payload, message_buffer)
通信相手に送信するための情報を生成して、それをmessage_bufferに設定します。具体的には、メッセージパターンに定 義されている公開鍵とpayloadです。
公開鍵とpayloadは必要に応じて暗号化されます。暗号化の際にはSymmetricState#EncryptAndHash(plaintext)が使用されます。
メッセージパターンが鍵交換を表す"ee", "es", "se", "ss"を含む場合には、DH関数を呼び出します。DH関数の結果をSymmetricState#MixKey(input_key_material)に渡して新しい暗号化鍵を生成します。
処理が終わった段階で、message_patternsから処理済みのメッセージパターンを除去します。すべてのメッセージパターンがなくなった時点で、SymmetricState#Split()を呼び出して、アプリケーションはハンドシェイクのフェーズを終了します。
ReadMessage(message, payload_buffer)
通信相手から受信したmessageを複合して、その結果をpayload_bufferに設定します。
WriteMessageと同様に、こちらでもメッセージパターンに定義されているトークンに応じた処理を行います。
処理が終わった段階で、message_patternsから処理済みのメッセージパターンを除去します。すべてのメッセージパターンがなくなった時点で、SymmetricState#Split()を呼び出して、アプリケーションはハンドシェイクのフェーズを終了します。